# -*- coding: utf-8 -*-

from odoo import http
from odoo.http import request
from .main import api_response, validate_token
from .auth import AuthController
import logging

_logger = logging.getLogger(__name__)


class VoteController(http.Controller):
    
    def _get_full_url(self, url):
        """将相对URL转换为完整URL"""
        if not url:
            return ''
        # 替换内部域名为外部域名
        if url.startswith('http://odoo'):
            url = url.replace('http://odoo', 'https://naturedao.tech')
            return url
        # 已经是完整的外部URL
        if url.startswith('https://'):
            return url
        # 使用固定的外部域名
        base_url = 'https://naturedao.tech'
        # 如果是相对路径，加上base URL
        if url.startswith('/'):
            return f'{base_url}{url}'
        return url

    @http.route('/api/v1/activities/<int:activity_id>/candidates', type='http', auth='public', methods=['GET'], csrf=False, cors='*')
    def get_candidates(self, activity_id, **kwargs):
        """获取候选人列表"""
        try:
            _logger.info(f'🔍 GET /activities/{activity_id}/candidates')
            
            # 获取已通过审核的报名
            candidates = request.env['speech.registration'].sudo().search([
                ('activity_id', '=', activity_id),
                ('status', '=', 'approved')
            ], order='vote_count desc' if kwargs.get('sort_by') == 'vote_count' else 'create_date')
            
            _logger.info(f'📊 找到 {len(candidates)} 个候选人')
            
            items = []
            for candidate in candidates:
                attachments = []
                for att in candidate.attachment_ids:
                    attachments.append({
                        'name': att.filename,
                        'url': att.file_url,
                    })
                
                items.append({
                    'id': candidate.id,
                    'name': candidate.name,
                    'phone': candidate.phone[:3] + '****' + candidate.phone[-4:] if candidate.phone else '',  # 脱敏
                    'topic': candidate.topic,
                    'introduction': candidate.introduction,
                    'vote_count': candidate.vote_count,
                    'attachments': attachments,
                })
            
            # 获取活动加油规则
            activity = request.env['speech.activity'].sudo().browse(activity_id)
            
            _logger.info(f'✅ 返回候选人列表: max_votes={activity.max_votes_per_user}')
            
            return request.make_json_response(api_response(True, {
                'items': items,
                'max_votes_per_user': activity.max_votes_per_user,
            }))
            
        except Exception as e:
            _logger.error(f'❌ 获取候选人失败: {str(e)}', exc_info=True)
            return request.make_json_response(api_response(False, message=f'获取失败: {str(e)}'))

    @http.route('/api/v1/activities/<int:activity_id>/votes', type='json', auth='public', methods=['POST'], csrf=False, cors='*')
    def submit_vote(self, activity_id, **kwargs):
        """提交加油（支持匿名加油，使用openid防重复）"""
        try:
            # 尝试获取Token（可选）
            token = request.httprequest.headers.get('Authorization', '').replace('Bearer ', '')
            _logger.info(f'🗳️ POST /activities/{activity_id}/votes: token={token[:20] if token else "匿名"}...')
            
            # 读取JSON请求体
            import json
            body_data = request.httprequest.get_data(as_text=True)
            data = json.loads(body_data) if body_data else {}
            
            # 尝试识别用户
            user = None
            voter_id = None  # 用于防重复加油的ID
            voter_name = '匿名用户'  # 默认匿名
            
            # 方式1：已登录用户（有token）
            if token:
                user = request.env['wechat.user'].sudo().search([('token', '=', token)], limit=1)
                if user and user.check_token(token):
                    voter_id = f'user_{user.id}'
                    voter_name = user.name
                    _logger.info(f'💚 已登录用户加油: {voter_name}')
            
            # 方式2：匿名加油（使用微信code换取openid）
            if not voter_id:
                wx_code = data.get('wx_code')
                openid = None
                
                if wx_code:
                    # 使用code换取openid
                    auth_controller = AuthController()
                    openid = auth_controller._get_wechat_openid(wx_code)
                    if openid:
                        _logger.info(f'✅ 从code获取openid: {openid[:8]}...')
                
                # 如果前端直接传了openid也接受
                if not openid:
                    openid = data.get('openid')
                
                if openid:
                    # 查找或创建该openid的匿名用户
                    user = request.env['wechat.user'].sudo().search([('openid', '=', openid)], limit=1)
                    if not user:
                        # 创建新的匿名用户
                        user = request.env['wechat.user'].sudo().create({
                            'name': f'匿名用户{openid[:8]}',
                            'openid': openid,
                            'phone': '',
                            'email': ''
                        })
                        _logger.info(f'✅ 创建匿名用户: openid={openid[:8]}...')
                    voter_id = f'openid_{openid}'
                    voter_name = user.name
                    _logger.info(f'👻 匿名用户加油: {voter_name}')
                else:
                    # 方式3：使用IP地址（最后备选，不推荐）
                    ip = request.httprequest.remote_addr
                    # 查找或创建该IP的匿名用户
                    user = request.env['wechat.user'].sudo().search([
                        ('name', '=', f'匿名用户_IP_{ip}'),
                        ('openid', '=', f'ip_{ip}')
                    ], limit=1)
                    if not user:
                        user = request.env['wechat.user'].sudo().create({
                            'name': f'匿名用户_IP_{ip}',
                            'openid': f'ip_{ip}',
                            'phone': '',
                            'email': ''
                        })
                    voter_id = f'ip_{ip}'
                    voter_name = user.name
                    _logger.info(f'🌐 使用IP加油: {ip}')
            
            # 现在user一定有值了（已登录用户或匿名用户）
            _logger.info(f'👤 最终加油用户: user_id={user.id}, name={voter_name}')
            
            candidate_ids = data.get('candidate_ids', [])
            
            if not candidate_ids:
                return api_response(False, message='请至少选择一位候选人')
            
            _logger.info(f'📝 加油: candidate_ids={candidate_ids}')
            
            # 检查候选人（报名记录）是否存在
            registrations = request.env['activity.item.registration'].sudo().browse(candidate_ids)
            if not all(reg.exists() for reg in registrations):
                return api_response(False, message='候选人不存在')
            
            # 检查是否已经给这个人加油过
            for candidate_id in candidate_ids:
                existing = request.env['speech.vote'].sudo().search([
                    ('activity_id', '=', activity_id),
                    ('user_id', '=', user.id),
                    ('registration_id', '=', candidate_id)
                ], limit=1)
                
                if existing:
                    # 获取报名人员姓名
                    reg = request.env['activity.item.registration'].sudo().browse(candidate_id)
                    return api_response(False, message=f'您已经给"{reg.name}"加油过了')
            
            # 新的加油验证：检查角色分类加油限制
            # 每个角色分类只能加油一次
            for candidate_id in candidate_ids:
                reg = request.env['activity.item.registration'].sudo().browse(candidate_id)
                category_id = reg.item_id.role_category_id.id if reg.item_id.role_category_id else None
                
                if category_id:
                    # 检查是否已在该角色分类中加油过
                    existing_in_category = request.env['speech.vote'].sudo().search([
                        ('activity_id', '=', activity_id),
                        ('user_id', '=', user.id),
                        ('registration_id.item_id.role_category_id', '=', category_id)
                    ], limit=1)
                    
                    if existing_in_category:
                        category_name = reg.item_id.role_category_id.name
                        return api_response(False, message=f'您已经在"{category_name}"分类中加油过了')
            
            # 为每个候选人创建加油记录
            for candidate_id in candidate_ids:
                request.env['speech.vote'].sudo().create({
                    'activity_id': activity_id,
                    'user_id': user.id,
                    'registration_id': candidate_id,
                    'ip_address': request.httprequest.remote_addr,
                })
            
            _logger.info(f'✅ 加油成功: user={user.name}, count={len(candidate_ids)}')
            
            return api_response(True, message='加油成功')
            
        except Exception as e:
            _logger.error(f'❌ 加油失败: {str(e)}', exc_info=True)
            return api_response(False, message=f'加油失败: {str(e)}')

    @http.route('/api/v1/activities/<int:activity_id>/votes/cancel', type='json', auth='public', methods=['POST'], csrf=False, cors='*')
    def cancel_vote(self, activity_id, **kwargs):
        """取消加油"""
        try:
            _logger.info(f'🗑️ POST /activities/{activity_id}/votes/cancel')
            
            # 读取JSON请求体（和加油接口一样的方式）
            import json
            body_data = request.httprequest.get_data(as_text=True)
            data = json.loads(body_data) if body_data else {}
            
            candidate_id = data.get('candidate_id')
            wx_code = data.get('wx_code')
            
            _logger.info(f'📋 请求体数据: {data}')
            _logger.info(f'📋 解析参数: candidate_id={candidate_id}, wx_code={wx_code[:10] if wx_code else "无"}...')
            
            if not candidate_id:
                _logger.error('❌ 缺少candidate_id参数')
                return api_response(False, message='缺少候选人ID')
            
            # 获取用户（支持已登录用户和匿名用户）
            user = None
            voter_id = None
            voter_name = '未知用户'
            
            # 方式1：已登录用户（有Token）
            token = request.httprequest.headers.get('Authorization', '').replace('Bearer ', '')
            if token:
                user = request.env['wechat.user'].sudo().search([('token', '=', token)], limit=1)
                if user and user.check_token(token):
                    voter_id = f'user_{user.id}'
                    voter_name = user.name
                    _logger.info(f'👤 已登录用户取消加油: {voter_name}')
            
            # 方式2：匿名用户（使用微信code换取openid）
            if not voter_id:
                openid = None
                
                if wx_code:
                    # 使用code换取openid
                    auth_controller = AuthController()
                    openid = auth_controller._get_wechat_openid(wx_code)
                    if openid:
                        _logger.info(f'✅ 从code获取openid: {openid[:8]}...')
                
                if openid:
                    # 查找该openid的用户
                    user = request.env['wechat.user'].sudo().search([('openid', '=', openid)], limit=1)
                    if user:
                        voter_id = f'openid_{openid}'
                        voter_name = user.name
                        _logger.info(f'👻 匿名用户取消加油: {voter_name}')
                else:
                    # 方式3：使用IP地址
                    ip = request.httprequest.remote_addr
                    user = request.env['wechat.user'].sudo().search([
                        ('name', '=', f'匿名用户_IP_{ip}'),
                        ('openid', '=', f'ip_{ip}')
                    ], limit=1)
                    if user:
                        voter_id = f'ip_{ip}'
                        voter_name = user.name
                        _logger.info(f'🌐 使用IP取消加油: {ip}')
            
            if not user:
                _logger.error('❌ 无法识别用户')
                return api_response(False, message='无法识别用户，请重新登录')
            
            # 查找加油记录
            vote = request.env['speech.vote'].sudo().search([
                ('activity_id', '=', activity_id),
                ('user_id', '=', user.id),
                ('registration_id', '=', candidate_id)
            ], limit=1)
            
            if not vote:
                _logger.warning(f'⚠️ 未找到加油记录: user={user.id}, candidate={candidate_id}')
                return api_response(False, message='未找到加油记录')
            
            # 删除加油记录
            candidate_name = vote.registration_id.name if vote.registration_id else '候选人'
            vote.sudo().unlink()
            
            _logger.info(f'✅ 取消加油成功: user={voter_name}, candidate={candidate_name}')
            
            return api_response(True, message='取消加油成功')
            
        except Exception as e:
            _logger.error(f'❌ 取消加油失败: {str(e)}', exc_info=True)
            return api_response(False, message=f'取消加油失败: {str(e)}')

    @http.route('/api/v1/activities/<int:activity_id>/votes/check', type='http', auth='public', methods=['GET'], csrf=False, cors='*')
    def check_voted(self, activity_id):
        """检查是否已加油"""
        try:
            # 手动验证Token（type='http'需要手动验证）
            token = request.httprequest.headers.get('Authorization', '').replace('Bearer ', '')
            _logger.info(f'🔍 GET /activities/{activity_id}/votes/check: token={token[:20] if token else "无"}...')
            
            if not token:
                _logger.warning('⚠️ 未提供Token')
                return request.make_json_response(api_response(False, message='请先登录', code=401))
            
            user = request.env['wechat.user'].sudo().search([('token', '=', token)], limit=1)
            
            if not user or not user.check_token(token):
                _logger.warning(f'⚠️ Token无效: {token[:20]}...')
                return request.make_json_response(api_response(False, message='登录已过期，请重新登录', code=401))
            
            _logger.info(f'💚 Token验证成功: 用户={user.name}')
            
            voted = bool(request.env['speech.vote'].sudo().search([
                ('activity_id', '=', activity_id),
                ('user_id', '=', user.id)
            ], limit=1))
            
            _logger.info(f'✅ 检查加油状态: voted={voted}')
            
            return request.make_json_response(api_response(True, {'voted': voted}))
            
        except Exception as e:
            _logger.error(f'❌ 检查加油失败: {str(e)}', exc_info=True)
            return request.make_json_response(api_response(False, message=f'检查失败: {str(e)}'))

    @http.route('/api/v1/user/votes', type='http', auth='public', methods=['GET'], csrf=False, cors='*')
    def my_votes(self, **kwargs):
        """获取我的加油记录"""
        try:
            # 手动验证Token（type='http'需要手动验证）
            token = request.httprequest.headers.get('Authorization', '').replace('Bearer ', '')
            _logger.info(f'🔍 GET /user/votes: token={token[:20] if token else "无"}...')
            
            if not token:
                _logger.warning('⚠️ 未提供Token')
                return request.make_json_response(api_response(False, message='请先登录', code=401))
            
            user = request.env['wechat.user'].sudo().search([('token', '=', token)], limit=1)
            
            if not user or not user.check_token(token):
                _logger.warning(f'⚠️ Token无效: {token[:20]}...')
                return request.make_json_response(api_response(False, message='登录已过期，请重新登录', code=401))
            
            _logger.info(f'💚 Token验证成功，查询用户{user.name}的加油记录')
            
            votes = request.env['speech.vote'].sudo().search([
                ('user_id', '=', user.id)
            ], order='create_date desc')
            
            _logger.info(f'📊 找到 {len(votes)} 条加油记录')
            
            items = []
            for vote in votes:
                candidates = []
                for line in vote.vote_line_ids:
                    candidates.append({
                        'id': line.candidate_id.id,
                        'name': line.candidate_id.name,
                    })
                
                items.append({
                    'id': vote.id,
                    'activity_name': vote.activity_id.title,
                    'candidate_count': len(candidates),
                    'candidates': candidates,
                    'created_at': vote.create_date.strftime('%Y-%m-%d %H:%M:%S') if vote.create_date else '',
                })
            
            _logger.info(f'✅ 返回 {len(items)} 条加油数据')
            
            return request.make_json_response(api_response(True, {
                'items': items,
                'total': len(items)
            }))
            
        except Exception as e:
            _logger.error(f'❌ 获取加油记录失败: {str(e)}', exc_info=True)
            return request.make_json_response(api_response(False, message=f'获取失败: {str(e)}'))

    @http.route('/api/v1/activities/<int:activity_id>/votes/results', type='http', auth='public', methods=['GET'], csrf=False, cors='*')
    def vote_results(self, activity_id, **kwargs):
        """获取加油结果（按角色分类分组）- 支持未登录用户"""
        try:
            _logger.info(f'🔍 GET /activities/{activity_id}/votes/results')
            
            # 获取所有角色分类
            categories = request.env['item.role.category'].sudo().search([], order='sequence')
            
            # 获取当前用户（支持已登录用户和匿名用户）
            current_user = None
            my_voted_category_ids = []  # 已加油的角色分类ID列表
            
            # 方式1：尝试从token获取已登录用户
            token = request.httprequest.headers.get('Authorization', '').replace('Bearer ', '')
            if token:
                user = request.env['wechat.user'].sudo().search([('token', '=', token)], limit=1)
                if user and user.check_token(token):
                    current_user = user
                    _logger.info(f'💚 已登录用户查询: {user.name}')
            
            # 方式2：如果没有token，尝试从wx_code获取匿名用户
            if not current_user:
                wx_code = kwargs.get('wx_code')
                if wx_code:
                    auth_controller = AuthController()
                    openid = auth_controller._get_wechat_openid(wx_code)
                    if openid:
                        user = request.env['wechat.user'].sudo().search([('openid', '=', openid)], limit=1)
                        if user:
                            current_user = user
                            _logger.info(f'👻 匿名用户查询: openid={openid[:8]}...')
            
            # 如果找到了用户，获取其已加油的角色分类
            if current_user:
                my_votes = request.env['speech.vote'].sudo().search([
                    ('activity_id', '=', activity_id),
                    ('user_id', '=', current_user.id)
                ])
                for vote in my_votes:
                    if vote.registration_id and vote.registration_id.item_id.role_category_id:
                        my_voted_category_ids.append(vote.registration_id.item_id.role_category_id.id)
            
            _logger.info(f'👤 用户已加油的角色分类: {my_voted_category_ids}')
            
            # 按角色分类组织数据
            result_categories = []
            for category in categories:
                # 获取此分类下的所有报名记录
                items = request.env['activity.registration.item'].sudo().search([
                    ('activity_id', '=', activity_id),
                    ('role_category_id', '=', category.id)
                ])
                
                candidates = []
                for item in items:
                    for reg in item.item_registration_ids:
                        if reg.status == 'approved':
                            # 统计此候选人的加油数
                            vote_count = request.env['speech.vote'].sudo().search_count([
                                ('activity_id', '=', activity_id),
                                ('registration_id', '=', reg.id)
                            ])
                            
                            # 检查当前用户是否给这个候选人加油过
                            has_voted = False
                            if current_user:
                                has_voted = bool(request.env['speech.vote'].sudo().search([
                                    ('activity_id', '=', activity_id),
                                    ('user_id', '=', current_user.id),
                                    ('registration_id', '=', reg.id)
                                ], limit=1))
                            
                            # 处理照片URL（确保是完整URL）
                            photo_url = self._get_full_url(reg.photo_url or '')
                            performance_urls = []
                            if reg.performance_photo_urls:
                                performance_urls = [self._get_full_url(url.strip()) for url in reg.performance_photo_urls.split(',') if url.strip()]
                            
                            candidates.append({
                                'id': reg.id,
                                'name': reg.name,
                                'item_id': item.id,
                                'item_name': item.name,
                                'photo_url': photo_url,
                                'performance_photo_urls': performance_urls,
                                'speech_intro': reg.speech_intro or '',
                                'vote_count': vote_count,
                                'has_voted': has_voted,
                            })
                
                # 排序：按加油数降序
                candidates_sorted = sorted(candidates, key=lambda x: x['vote_count'], reverse=True)
                
                result_categories.append({
                    'id': category.id,
                    'name': category.name,
                    'candidates': candidates_sorted,
                    'user_has_voted_in_category': category.id in my_voted_category_ids,
                })
            
            _logger.info(f'✅ 返回 {len(result_categories)} 个角色分类')
            
            return request.make_json_response(api_response(True, {
                'categories': result_categories,
                'my_voted_count': len(my_voted_category_ids),
            }))
            
        except Exception as e:
            _logger.error(f'❌ 获取加油结果失败: {str(e)}', exc_info=True)
            return request.make_json_response(api_response(False, message=f'获取失败: {str(e)}'))
    
    @http.route('/api/v1/activities/<int:activity_id>/votes/export', type='http', auth='public', methods=['GET'], csrf=False, cors='*')
    def export_votes_excel(self, activity_id, **kwargs):
        """导出加油记录为Excel"""
        try:
            _logger.info(f'📊 GET /activities/{activity_id}/votes/export')
            
            # 验证Token和管理员权限（支持Header和URL参数两种方式）
            token = request.httprequest.headers.get('Authorization', '').replace('Bearer ', '')
            if not token:
                token = kwargs.get('token', '')
            
            if not token:
                return request.make_json_response(api_response(False, message='请先登录', code=401))
            
            _logger.info(f'🔑 使用token: {token[:20]}...')
            
            user = request.env['wechat.user'].sudo().search([('token', '=', token)], limit=1)
            
            if not user or not user.check_token(token):
                _logger.warning(f'⚠️ Token验证失败')
                return request.make_json_response(api_response(False, message='登录已过期', code=401))
            
            # 检查是否是管理员
            activity = request.env['speech.activity'].sudo().browse(activity_id)
            is_manager = user.id == activity.creator_id.id or user in activity.manager_ids
            
            if not is_manager:
                _logger.warning(f'⚠️ 权限不足: user={user.name}')
                return request.make_json_response(api_response(False, message='权限不足，仅管理员可导出', code=403))
            
            _logger.info(f'✅ 管理员验证成功: {user.name}')
            
            # 生成Excel
            from io import BytesIO
            import xlsxwriter
            
            output = BytesIO()
            workbook = xlsxwriter.Workbook(output)
            worksheet = workbook.add_worksheet('加油记录')
            
            # 设置格式
            header_format = workbook.add_format({
                'bold': True,
                'bg_color': '#4472C4',
                'font_color': 'white',
                'align': 'center',
                'valign': 'vcenter',
                'border': 1
            })
            
            cell_format = workbook.add_format({
                'align': 'left',
                'valign': 'vcenter',
                'border': 1
            })
            
            # 写入标题
            worksheet.write(0, 0, f'会议名称：{activity.title}', header_format)
            worksheet.merge_range(0, 0, 0, 10, f'会议名称：{activity.title}', header_format)
            
            # 写入表头
            headers = ['角色分类', '项目名称', '姓名', '演讲介绍', '报名照片', '风采照片1', '风采照片2', '风采照片3', '加油数', '排名', '加油人员']
            for col, header in enumerate(headers):
                worksheet.write(1, col, header, header_format)
            
            # 获取数据按角色分类
            categories = request.env['item.role.category'].sudo().search([], order='sequence')
            row = 2
            
            for category in categories:
                items = request.env['activity.registration.item'].sudo().search([
                    ('activity_id', '=', activity_id),
                    ('role_category_id', '=', category.id)
                ])
                
                # 收集此分类的所有候选人
                candidates = []
                for item in items:
                    for reg in item.item_registration_ids:
                        if reg.status == 'approved':
                            # 获取加油记录和加油人员
                            votes = request.env['speech.vote'].sudo().search([
                                ('activity_id', '=', activity_id),
                                ('registration_id', '=', reg.id)
                            ])
                            vote_count = len(votes)
                            
                            # 获取加油人员名单
                            voters = ', '.join([vote.user_id.name for vote in votes if vote.user_id])
                            
                            candidates.append({
                                'category_name': category.name,
                                'item_name': item.name,
                                'name': reg.name,
                                'speech_intro': reg.speech_intro or '',
                                'photo_url': reg.photo_url or '',
                                'performance_photo_urls': reg.performance_photo_urls or '',
                                'vote_count': vote_count,
                                'voters': voters,
                            })
                
                # 按加油数排序
                candidates_sorted = sorted(candidates, key=lambda x: x['vote_count'], reverse=True)
                
                # 写入数据
                for rank, candidate in enumerate(candidates_sorted, 1):
                    # 设置行高（以容纳图片，1像素≈0.75点）
                    worksheet.set_row(row, 60)  # 60点 ≈ 80像素
                    
                    worksheet.write(row, 0, candidate['category_name'], cell_format)
                    worksheet.write(row, 1, candidate['item_name'], cell_format)
                    worksheet.write(row, 2, candidate['name'], cell_format)
                    worksheet.write(row, 3, candidate['speech_intro'], cell_format)
                    
                    # 插入报名照片
                    if candidate['photo_url']:
                        try:
                            photo_url = self._get_full_url(candidate['photo_url'])
                            _logger.info(f'📸 处理报名照片: {photo_url}')
                            
                            if photo_url.startswith('https://'):
                                import requests
                                import io
                                
                                photo_response = requests.get(photo_url, timeout=10)
                                if photo_response.status_code == 200:
                                    image_data = io.BytesIO(photo_response.content)
                                    worksheet.insert_image(row, 4, photo_url, {
                                        'image_data': image_data,
                                        'x_scale': 0.15,
                                        'y_scale': 0.15,
                                        'x_offset': 5,
                                        'y_offset': 5
                                    })
                                    _logger.info(f'✅ 报名照片插入成功')
                                else:
                                    _logger.warning(f'⚠️ 下载照片失败: {photo_response.status_code}')
                                    worksheet.write(row, 4, candidate['photo_url'], cell_format)
                            else:
                                _logger.info(f'⚠️ 照片URL不是https，写入链接: {photo_url}')
                                worksheet.write(row, 4, candidate['photo_url'], cell_format)
                        except Exception as e:
                            _logger.error(f'❌ 插入报名照片失败: {str(e)}')
                            worksheet.write(row, 4, candidate['photo_url'], cell_format)
                    
                    # 插入风采照片（最多3张）
                    perf_urls = []
                    if candidate['performance_photo_urls']:
                        perf_urls = [url.strip() for url in candidate['performance_photo_urls'].split(',') if url.strip()]
                    
                    for i in range(3):
                        col = 5 + i
                        if i < len(perf_urls):
                            try:
                                perf_url = self._get_full_url(perf_urls[i])
                                _logger.info(f'📸 处理风采照片{i+1}: {perf_url}')
                                
                                if perf_url.startswith('https://'):
                                    import requests
                                    import io
                                    
                                    perf_response = requests.get(perf_url, timeout=10)
                                    if perf_response.status_code == 200:
                                        perf_image_data = io.BytesIO(perf_response.content)
                                        worksheet.insert_image(row, col, perf_url, {
                                            'image_data': perf_image_data,
                                            'x_scale': 0.15,
                                            'y_scale': 0.15,
                                            'x_offset': 5,
                                            'y_offset': 5
                                        })
                                        _logger.info(f'✅ 风采照片{i+1}插入成功')
                                    else:
                                        _logger.warning(f'⚠️ 下载风采照片{i+1}失败: {perf_response.status_code}')
                                        worksheet.write(row, col, perf_urls[i], cell_format)
                                else:
                                    _logger.info(f'⚠️ 风采照片{i+1}不是https，写入链接')
                                    worksheet.write(row, col, perf_urls[i], cell_format)
                            except Exception as e:
                                _logger.error(f'❌ 插入风采照片{i+1}失败: {str(e)}')
                                worksheet.write(row, col, perf_urls[i], cell_format)
                        else:
                            worksheet.write(row, col, '', cell_format)
                    
                    worksheet.write(row, 8, candidate['vote_count'], cell_format)
                    worksheet.write(row, 9, rank, cell_format)
                    worksheet.write(row, 10, candidate['voters'], cell_format)
                    row += 1
            
            # 设置列宽
            worksheet.set_column(0, 0, 15)  # 角色分类
            worksheet.set_column(1, 1, 20)  # 项目名称
            worksheet.set_column(2, 2, 15)  # 姓名
            worksheet.set_column(3, 3, 40)  # 演讲介绍
            worksheet.set_column(4, 4, 12)  # 报名照片
            worksheet.set_column(5, 7, 12)  # 风采照片1-3
            worksheet.set_column(8, 8, 10)  # 加油数
            worksheet.set_column(9, 9, 10)  # 排名
            worksheet.set_column(10, 10, 40)  # 加油人员
            
            workbook.close()
            output.seek(0)
            
            # 返回Excel文件
            from odoo import fields
            from urllib.parse import quote
            
            filename = f'加油记录_{activity.title}_{fields.Datetime.now().strftime("%Y%m%d_%H%M%S")}.xlsx'
            # URL编码文件名以支持中文（RFC 5987）
            filename_encoded = quote(filename)
            
            return request.make_response(
                output.getvalue(),
                headers=[
                    ('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'),
                    ('Content-Disposition', f"attachment; filename*=UTF-8''{filename_encoded}")
                ]
            )
            
        except Exception as e:
            _logger.error(f'❌ 导出Excel失败: {str(e)}', exc_info=True)
            return request.make_json_response(api_response(False, message=f'导出失败: {str(e)}'))
    
    @http.route('/api/v1/activities/<int:activity_id>/vote-wxacode', type='http', auth='public', methods=['GET'], csrf=False, cors='*')
    def generate_vote_wxacode(self, activity_id):
        """生成加油页面的小程序码"""
        try:
            _logger.info(f'📱 GET /activities/{activity_id}/vote-wxacode')
            
            activity = request.env['speech.activity'].sudo().browse(activity_id)
            
            if not activity.exists():
                return request.make_json_response(api_response(False, message='会议不存在', code=404))
            
            # 获取access_token
            auth_controller = AuthController()
            access_token = auth_controller._get_wechat_access_token()
            
            if not access_token:
                _logger.error('❌ 获取access_token失败')
                return request.make_json_response(api_response(False, message='生成失败，请稍后重试'))
            
            # 调用微信生成小程序码接口
            import requests
            import json
            
            url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}'
            
            # 使用scene参数传递activity_id（加油页面）
            data = {
                'scene': f'vote={activity_id}',  # 加油页面专用scene
                'page': 'pages/activity/vote/vote',  # 加油页面
                'check_path': False,  # 不检查页面路径
                'env_version': 'release',  # 正式版（release=正式版, trial=体验版, develop=开发版）
                'width': 430
            }
            
            _logger.info(f'🔑 生成加油小程序码: scene={data["scene"]}')
            
            response = requests.post(url, json=data, timeout=10)
            
            # 检查返回类型
            content_type = response.headers.get('Content-Type', '')
            
            if 'image' in content_type:
                # 成功返回图片
                _logger.info(f'✅ 加油小程序码生成成功')
                return request.make_response(
                    response.content,
                    headers=[
                        ('Content-Type', 'image/jpeg'),
                        ('Content-Disposition', f'inline; filename=vote_{activity_id}_wxacode.jpg')
                    ]
                )
            else:
                # 返回错误信息
                result = response.json()
                _logger.error(f'❌ 生成加油小程序码失败: {result}')
                return request.make_json_response(api_response(False, message=result.get('errmsg', '生成失败')))
            
        except Exception as e:
            _logger.error(f'❌ 生成加油小程序码异常: {str(e)}', exc_info=True)
            return request.make_json_response(api_response(False, message=f'生成失败: {str(e)}'))

